
let {SyncHook} = require('tapable');
let fs = require('fs');
let path = require('path');
let {toUnixPath} = require('./utils');
let types = require('babel-types');//判断某个节点是否是某种类型，生成某个新的节点
let parser = require('@babel/parser');//把源码生成AST语法树
let traverse = require('@babel/traverse').default;//遍历器，用来遍历语法树的
let generator = require('@babel/generator').default;//生成器，根据语法树重新生成代码
let rootPath = toUnixPath(process.cwd());
class Compiler{
    constructor(options){
        this.options = options;
        this.hooks = {
            run:new SyncHook(),//开启编译
            emit:new SyncHook(),//这个钩子会在我们准备写入文件系统的时候触发
            done:new SyncHook()//编译工作全部完成
        }
        this.entries = new Set();//所有的入口模块 webpack4 数组 webpack4 Set
        this.modules = new Set();//所有的模块
        this.chunks = new Set();//所有的代码块
        this.assets = {};//存放着本次要产出的文件
        this.files = new Set();//存放着本次编译所有的产出的文件名
    }
    run(callback){
        this.hooks.run.call();
        //5.根据配置中的entry找出入口文件
        let entry = {};
        if(typeof this.options.entry === 'string'){
            entry.main = this.options.entry;
        }else{
            entry=this.options.entry;
        }
        
        for(let entryName in entry){
            let entryPath = toUnixPath(path.join(rootPath,entry[entryName]));
            //6.从入口文件出发,调用所有配置的Loader对模块进行编译
            let entryModule = this.buildModule(entryName,entryPath);
            this.entries.add(entryModule);
            //this.modules.add(entryModule);
             //8.根据入口和模块之间的依赖关系，组装成一个个包含多个模块的 Chunk
            let chunk = {name:entryName,entryModule,modules:Array.from(this.modules).filter(
                module=>module.name === entryName
            )};
            this.chunks.add(chunk);
        }
        //9.再把每个Chunk转换成一个单独的文件加入到输出列表 this.assets对象key文件名 值 文件的内容
        let output = this.options.output;
        this.chunks.forEach(chunk=>{
            let filename = output.filename.replace('[name]',chunk.name);//filename只是文件名
            this.assets[filename] = getSource(chunk);
        });
        this.hooks.emit.call();
        this.files = Object.keys(this.assets);//文件名的一个数组
        for(let filename in this.assets){
            let filePath = path.join(output.path,filename);
            fs.writeFileSync(filePath,this.assets[filename]);
        }
        //到了这里编译工作就全部结束，就可以触发done的回调了
        this.hooks.done.call();
        callback(null,{
            toJson:()=>(
                {
                    entries:this.entries,
                    chunks:this.chunks,
                    modules:this.modules,
                    files:this.files,
                    assets:this.assets
                }
            )
        });

       
    }
    buildModule(entryName,modulePath){
        //1.读取出来此模块的内容 
        let originalSourceCode = fs.readFileSync(modulePath,'utf8');
        let targetSourceCode = originalSourceCode;
        //2.调用所有配置的Loader对模块进行编译
        let rules = this.options.module.rules;
        //得到了本文件模块生效的loader有哪些
        let loaders = [];
        for(let i=0;i<rules.length;i++){
            //if(rules[i].test.test(modulePath)){
            if(modulePath.match(rules[i].test)){
                loaders = [...loaders,...rules[i].use];
            }
        }
        for(let i=loaders.length-1;i>=0;i--){
            let loaderObject = loaders[i];//use =[{loader:'',options:{}}]
            let loaderName =  loaderObject.loader;
            let loaderOptions = loaderObject.options;
            //loader runner
            targetSourceCode=require(loaders[i])(targetSourceCode);
        }
        //7.再找出该模块依赖的模块，再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
        //A B C 模块ID都是一个相对于根目录的相对路径 ./src/entry1.js
        let moduleId = './'+path.posix.relative(rootPath,modulePath);
        let module = {id:moduleId,dependencies:[],name:entryName};
        //再找出该模块依赖的模块 把转换后的源码转成抽象语法树
        let ast = parser.parse(targetSourceCode,{sourceType:'module'});
        traverse(ast,{
            CallExpression:({node})=>{
                if(node.callee.name === 'require'){
                    //要引入的模块相对路径
                    let moduleName = node.arguments[0].value;//"./title"
                    //为了获取要加载的模块的绝对路径 第一步获得当前模块的所在目录
                    let dirName = path.posix.dirname(modulePath);
                    let depModulePath = path.posix.join(dirName,moduleName);
                    let extensions = this.options.resolve.extensions;
                    depModulePath = tryExtensions(depModulePath,extensions,moduleName,dirName);
                    let depModuleId = './'+path.posix.relative(rootPath,depModulePath);//./src/title.js
                    node.arguments =[types.stringLiteral(depModuleId)];
                    //判断现有的已经编译过的modules里有没有这个模块，如果有，不用添加依赖了，如果没有则需要添加
                    let alreadyModuleIds = Array.from(this.modules).map(module=>module.id);
                    //如果已经编译过的模块的里不包含这个依赖模块的话才添加，如果已经包含了，就不要添加了
                    if(!alreadyModuleIds.includes(depModuleId)){
                        module.dependencies.push(depModulePath);
                    }
                }
            }
        });
        let {code} = generator(ast);
        module._source = code;//此模块的源代码
        //把当前的模块编译完成，会找到它的所有推中，进行递归的编译，添加到this.modules就可以了
        module.dependencies.forEach(dependency=>{
            let depModule = this.buildModule(entryName,dependency);
            this.modules.add(depModule);
        });
        return module;
    }
}
/**
 * 获取chunk对应的源代码 输出的文件内容
 * @param {*} chunk 
 *    name 代码块的名字 entryModule入口模块 modules所有的模块
 */
function getSource(chunk){
    return `
    (() => {
        var modules = ({
           ${
               chunk.modules.map(module=>`
               "${module.id}":
                ((module) => {
                          ${module._source}
                })
               `).join(',')
           } 
        });
        var cache = {};
        function require(moduleId) {
          var cachedModule = cache[moduleId];
          if (cachedModule !== undefined) {
            return cachedModule.exports;
          }
          var module = cache[moduleId] = {
            exports: {}
          };
          modules[moduleId](module, module.exports, require);
          return module.exports;
        }
        var exports = {};
        (() => {
                ${chunk.entryModule._source}
        })();
      })()
        ;
    `;
}
/**
 * 
 * @param {*} modulePath 拼出来的模块的路径 c:/src/title
 * @param {*} extensions  ['.js','.jsx','.json']
 * @param {*} originModulePath ./title
 * @param {*} moduleContext  c:/src
 */
function tryExtensions(modulePath,extensions,originModulePath,moduleContext){
  extensions.unshift('');//['','.js','.jsx','.json']
  for(let i=0;i<extensions.length;i++){
      if(fs.existsSync(modulePath+extensions[i])){
        return modulePath+extensions[i];
      }
  }
  //如果到了这句话还执行到了，立明没有一个后缀能匹配上
  throw new Error(`Module not found: Error: Can't resolve ${originModulePath} in ${moduleContext}`);
}
module.exports = Compiler;